package com.dysoft.flesh.gl;

import com.dysoft.bones.DataManager;
import com.dysoft.flesh.RenderContext;
import com.dysoft.flesh.RenderTarget;
import com.dysoft.flesh.Texture;

import javax.media.opengl.GL;
import javax.media.opengl.GLContext;
import javax.media.opengl.GLException;
import javax.media.opengl.glu.GLU;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;

/**
 * @author Sean Micklethwaite
 *         Aug 14, 2010 10:28:34 PM
 *
 * FBO Wrapper Class
 */
public class GLFrameBuffer extends GLObject implements RenderTarget {
	private static final int MAX_BUFFERS = 32;
	private static final int [] ATTACHMENTS = new int [MAX_BUFFERS];

	static {
		for(int i = 0; i < MAX_BUFFERS; i++)
			ATTACHMENTS[i] = GL.GL_COLOR_ATTACHMENT0_EXT + i;
	}

	final List<Texture> textures = new ArrayList<Texture>();
	final int w, h;
	final GLObject depthBuffer;
	final GLU glu;
	final boolean depth;
	final int bpp;

	boolean depthEnabled;

	public GLFrameBuffer(int w, int h, int bpp, boolean depth) {
		super(genFBO(GLContext.getCurrent().getGL()));
		this.w = w;
		this.h = h;
		this.glu = new GLU();
		this.depth = depth;
		this.bpp = bpp;

		GL gl = GLContext.getCurrent().getGL();
		gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, handle);
		
		if(depth) {
			final IntBuffer ib = IntBuffer.allocate(1);
			gl.glGenRenderbuffersEXT(1, ib);
			depthBuffer = new GLObject(ib.get(0)) {
				@Override
				protected void finalize() throws Throwable {
					ib.flip();
					//gl.glDeleteRenderbuffersEXT(1, ib); TODO
					super.finalize();
				}
			};

			gl.glBindRenderbufferEXT(GL.GL_RENDERBUFFER_EXT, depthBuffer.getHandle());
			gl.glRenderbufferStorageEXT(GL.GL_RENDERBUFFER_EXT, GL.GL_DEPTH_COMPONENT, w, h);
			gl.glBindRenderbufferEXT(GL.GL_RENDERBUFFER_EXT, 0);

			gl.glFramebufferRenderbufferEXT(GL.GL_FRAMEBUFFER_EXT, GL.GL_DEPTH_ATTACHMENT_EXT,
					GL.GL_RENDERBUFFER_EXT, depthBuffer.getHandle());
		} else {
			depthBuffer = null;
		}
		
		gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, 0);
	}

	public GLFrameBuffer(int w, int h, int bpp) {
		this(w, h, bpp, false);
	}

	public Texture addTexture(int channels, boolean floatFmt) throws Exception {
		if(textures.size() == MAX_BUFFERS) {
			throw new RuntimeException("Maximum textures per FBO is " + MAX_BUFFERS);
		}

		GL gl = GLContext.getCurrent().getGL();

		TextureImpl tex= new TextureImpl(gl, this.bpp, channels, floatFmt);
		textures.add(tex);

		gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, handle);
		gl.glFramebufferTexture2DEXT( GL.GL_FRAMEBUFFER_EXT,
				GL.GL_COLOR_ATTACHMENT0_EXT + (textures.size() - 1),
				GL.GL_TEXTURE_2D, tex.getHandle(), 0);
		int status = gl.glCheckFramebufferStatusEXT(GL.GL_FRAMEBUFFER_EXT);
		if(status != GL.GL_FRAMEBUFFER_COMPLETE_EXT) {
			throw new GLException("FBO Not Supported!");
		}
		gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, 0);

		return tex;
	}

	public Texture addTexture(int channels) throws Exception {
		return addTexture(channels, true);
	}

	public void activate(RenderContext ctx) {
		GL gl = ((GLRenderer.GLRenderContext)ctx).getGL();

		gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, handle);
		gl.glPushAttrib(GL.GL_VIEWPORT_BIT);
		gl.glViewport(0, 0, w, h);
		gl.glDrawBuffers(textures.size(), ATTACHMENTS, 0);

		depthEnabled = gl.glIsEnabled(GL.GL_DEPTH_TEST);
		if(depth && !depthEnabled)
			gl.glEnable(GL.GL_DEPTH_TEST);
		else if(!depth && depthEnabled)
			gl.glDisable(GL.GL_DEPTH_TEST);
	}

	public void deactivate(RenderContext ctx) {
		GL gl = ((GLRenderer.GLRenderContext)ctx).getGL();

		if(depth && !depthEnabled)
			gl.glDisable(GL.GL_DEPTH_TEST);
		else if(!depth && depthEnabled) {
			gl.glEnable(GL.GL_DEPTH_TEST);
		}

		gl.glPopAttrib();
		gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, 0);
	}

	int genTex(GL gl) {
		IntBuffer ib = IntBuffer.allocate(1);
		gl.glGenTextures(1, ib);
		return ib.get(0);
	}

	public int getWidth() {
		return w;
	}

	public int getHeight() {
		return h;
	}

	@Override
	protected void finalize() throws Throwable {
		//gl.glDeleteFramebuffersEXT(1, new int [] { handle }, 0);
		super.finalize();
	}

	public List<Texture> getTextures() {
		return textures;
	}

	class TextureImpl extends GLObject implements Texture {
		TextureImpl(GL gl, int bpp, int channels, boolean floatFmt) throws Exception {
			super(genTex(gl));

			gl.glBindTexture(GL.GL_TEXTURE_2D, handle);
			gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP);
			gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP);
			gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
			gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
			gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_GENERATE_MIPMAP, GL.GL_FALSE);
			gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, GLTexture.getTextureFormat(bpp, channels, floatFmt), w, h, 0, GL.GL_RGBA,
					floatFmt ? GL.GL_FLOAT : GL.GL_UNSIGNED_INT, null);
			gl.glBindTexture(GL.GL_TEXTURE_2D, 0);
		}

		public void bind(RenderContext ctx, int unit) {
			GL gl = ((GLRenderer.GLRenderContext)ctx).getGL();
			gl.glActiveTexture(GL.GL_TEXTURE0 + unit);
			gl.glBindTexture(GL.GL_TEXTURE_2D, handle);
		}

		public void unbind(RenderContext ctx, int unit) {
			GL gl = ((GLRenderer.GLRenderContext)ctx).getGL();
			gl.glActiveTexture(GL.GL_TEXTURE0 + unit);
			gl.glBindTexture(GL.GL_TEXTURE_2D, 0);
		}

		public void render(RenderContext ctx) throws DataManager.DataObject.NotPresentException {
			GLTexture.renderQuad(ctx, this);
		}

		@Override
		protected void finalize() throws Throwable {
			//gl.glDeleteTextures(1, new int [] { handle }, 0); TODO
			super.finalize();
		}
	}

	private static int genFBO(GL gl) {
		IntBuffer buf = IntBuffer.allocate(1);
		gl.glGenFramebuffersEXT(1, buf);
		return buf.get(0);
	}
}
